package rad.framework.seam;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;

import javax.faces.model.SelectItem;
import javax.persistence.EntityManager;

import rad.framework.jpa.EntityPersistenceContextMap;

import org.apache.commons.lang.StringUtils;

/**
 * This class holds metadata for and generates where clauses for the finder
 * beans. It is also serializable so that instances can be saved by users.
 * 
 * @author boudyacho
 */
public class Filter implements Serializable {
	private static final long serialVersionUID = 1L;

	private Long id;

	public Long getId() {
		return id;
	}

	public void setId(Long id) {
		this.id = id;
	}

	private String name;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	/**
	 * Returns a where clause of various operators and'd together
	 */
	public String getFilterString(Map<String, Object> realparams) {
		StringBuffer sb = new StringBuffer();
		List<Parameter> params = getAllParameters();
		for (Parameter parameter : params) {
			String s = parameter.getFilterString(realparams);
			if (s.length() > 0 && sb.length() > 0) {
				sb.append(" and ");
			}
			sb.append(s);
		}
		return sb.toString();
	}

	private List<Parameter> getAllParameters() {
		List<Parameter> list = new ArrayList<Parameter>();
		list.addAll(strings);
		list.addAll(dates);
		list.addAll(booleans);
		list.addAll(numbers);
		list.addAll(enums);
		list.addAll(m2os);
		list.addAll(m2ms);
		list.addAll(o2ms);
		return list;
	}

	public String getJoins() {
		StringBuilder join = new StringBuilder();
		for (Parameter parameter : getAllParameters()) {
			parameter.getJoin(join);
		}
		return join.toString();
	}

	public void clear() {
		id = null;
		name = null;
		List<Parameter> params = getAllParameters();
		for (Parameter parameter : params) {
			parameter.clear();
		}
	}

	public boolean isDistinct() {
		List<Parameter> all = getAllParameters();
		for (Parameter parameter : all) {
			if (parameter.hasValue() && parameter.isAssociation()) {
				return true;
			}
		}
		return false;
	}
	
	// -------------------------------------------------------
	// metadata setup utils
	// -------------------------------------------------------

	/**
	 * @param name
	 *            the property to search on
	 */
	public void addStringParameter(String name) {
		getStrings().add(new StringParameter(name));
	}

	public void addStringParameter(String name, String association, String label) {
		getStrings().add(new StringParameter(name, association, label));
	}

	/**
	 * @param name
	 *            the property to search on
	 */
	public void addBooleanParameter(String name) {
		getBooleans().add(new BooleanParameter(name));
	}

	public void addBooleanParameter(String name, String association, String label) {
		getBooleans().add(new BooleanParameter(name, association, label));
	}

	/**
	 * @param name
	 *            the property to search on
	 */
	public void addDateParameter(String name) {
		getDates().add(new DateParameter(name));
	}

	public void addDateParameter(String name, String association, String label) {
		getDates().add(new DateParameter(name, association, label));
	}

	/**
	 * @param name
	 *            the property to search on
	 */
	public void addNumberParameter(String name) {
		getNumbers().add(new NumberParameter(name));
	}

	public void addNumberParameter(String name, String association, String label) {
		getNumbers().add(new NumberParameter(name, association, label));
	}

	/**
	 * @param name
	 *            the property to search on
	 * @param clazz
	 *            the enum class
	 */
	public void addEnumParameter(String name, Class clazz) {
		getEnums().add(new EnumParameter(name, clazz));
	}

	public void addEnumParameter(String name, String association, String label, Class clazz) {
		getEnums().add(new EnumParameter(name, association, label, clazz));
	}

	/**
	 * @param name
	 *            the association to search on
	 * @param property
	 *            the property on the associated entity to search on
	 * @param eql
	 *            an eql such as - select id, name from Project order by name
	 */
	public EntityParameter addM2oParameter(String name, String property, String eql) {
		EntityParameter ep = new EntityParameter(name, property, eql); 
		getM2os().add(ep);
		return ep;
	}

	public EntityParameter addM2oParameter(String name, String label, String property,
			String eql) {
		EntityParameter ep = new EntityParameter(name, label, property, eql); 
		getM2os().add(new EntityParameter(name, label, property, eql));
		return ep;
	}

	/**
	 * @param name
	 *            the association to search on
	 * @param property
	 *            the property on the associated entity to search on
	 * @param eql
	 *            an eql such as - select id, name from Project order by name
	 */
	public M2mEntityParameter addM2mParameter(String name, String property, String eql) {
		M2mEntityParameter ep = new M2mEntityParameter(name, property, eql); 
		getM2ms().add(new M2mEntityParameter(name, property, eql));
		return ep;
	}

	public M2mEntityParameter addM2mParameter(String name, String label, String property,
			String eql) {
		M2mEntityParameter ep = new M2mEntityParameter(name, label, property, eql); 
		getM2ms().add(new M2mEntityParameter(name, label, property, eql));
		return ep;
	}

	/**
	 * @param name
	 *            the association to search on
	 * @param property
	 *            the property on the associated entity to search on
	 * @param eql
	 *            an eql such as - select id, name from Project order by name
	 */
	public O2mEntityParameter addO2mParameter(String name, String property, String eql) {
		O2mEntityParameter ep = new O2mEntityParameter(name, property, eql); 
		getO2ms().add(new O2mEntityParameter(name, property, eql));
		return ep;
	}

	public O2mEntityParameter addO2mParameter(String name, String label, String property,
			String eql) {
		O2mEntityParameter ep = new O2mEntityParameter(name, label, property, eql); 
		getO2ms().add(new O2mEntityParameter(name, label, property, eql));
		return ep;
	}

	// -------------------------------------------------------
	// Parameter classes
	// -------------------------------------------------------

	public class Parameter implements Serializable {
		private static final long serialVersionUID = 1L;

		private String name;
		private String label;
		private String association;

		public Parameter() {

		}

		public Parameter(String name) {
			this.name = name;
			this.label = getResourceBundle().getString(entityClass.getSimpleName()
					+ "_" + name);
		}

		public Parameter(String name, String association, String label) {
			this.name = name;
			this.label = getResourceBundle().getString(label);
			this.association = association;
		}

		public String getName() {
			if (isAssociation()) {
				return association + "." + name;
			}
			return name;
		}

		public void setName(String name) {
			this.name = name;
		}

		public String getParameterName() {
			if (isAssociation()) {
				return association + name;
			}
			return name;
		}

		public String getLabel() {
			return label;
		}

		public void setLabel(String label) {
			this.label = label;
		}

		public String getAssociation() {
			return association;
		}

		public void setAssociation(String association) {
			this.association = association;
		}
		
		protected boolean isAssociation() {
			return !StringUtils.isEmpty(association);
		}

		protected String getAlias() {
			if (isAssociation()) {
				return "";
			} else {
				return "t.";
			}
		}
		
		protected boolean hasValue() {
			return false;
		}
		
		public String getFilterString(Map<String, Object> realparams) {
			return "";
		}

		public void getJoin(StringBuilder join) {
			if (isAssociation()) {
				if (hasValue()) {
					String temp = " join t." + association + " " + association;
					if (join.indexOf(temp) == -1) {
						join.append(temp);
					}
				}
			}
		}

		public void clear() {
		}
	}

	public class StringParameter extends Parameter {
		private String value;

		public StringParameter(String name) {
			super(name);
		}

		public StringParameter() {
			super();
		}

		public StringParameter(String name, String association, String label) {
			super(name, association, label);
		}

		public String getValue() {
			return value;
		}

		public void setValue(String value) {
			this.value = value;
		}
		
		protected boolean hasValue() {
			return !StringUtils.isEmpty(value);
		}
		
		public String getFilterString(Map<String, Object> realparams) {
			if (!hasValue()) {
				return "";
			}
			realparams.put(getParameterName(), "%" + getValue().toLowerCase().replace("%", "\\%") + "%");
			return "lower(" + getAlias() + getName() + ") like :" + getParameterName() + " ESCAPE '\\'";
		}

		public void clear() {
			value = null;
		}
	}

	public class BooleanParameter extends Parameter {

		private String value = "EITHER";

		public BooleanParameter(String name) {
			super(name);
		}

		public BooleanParameter() {
			super();
		}

		public BooleanParameter(String name, String association, String label) {
			super(name, association, label);
		}

		public String getValue() {
			return value;
		}

		public void setValue(String value) {
			this.value = value;
		}
		
		protected boolean hasValue() {
			return value != null && !"EITHER".equals(value);
		}

		protected Boolean getBooleanValue() {
			if ("TRUE".equals(value)) {
				return Boolean.TRUE;
			}
			return Boolean.FALSE;
		}

		public String getFilterString(Map<String, Object> realparams) {
			if (!hasValue()) {
				return "";
			}
			realparams.put(getParameterName(), getBooleanValue());
			return getAlias() + getName() + " = :" + getParameterName();
		}

		public void clear() {
			value = "EITHER";
		}
	}

	public class DateParameter extends Parameter {
		private Date lh;
		private Date rh;

		public DateParameter(String name) {
			super(name);
		}

		public DateParameter() {
			super();
		}

		public DateParameter(String name, String association, String label) {
			super(name, association, label);
		}

		public Date getLh() {
			return lh;
		}

		public void setLh(Date lh) {
			this.lh = lh;
		}

		public Date getRh() {
			return rh;
		}

		public void setRh(Date rh) {
			this.rh = rh;
		}

		protected boolean hasValue() {
			return lh != null || rh != null;
		}
		
		public String getFilterString(Map<String, Object> realparams) {
			if (!hasValue()) {
				return "";
			}
			if (lh == null) {
				realparams.put(getParameterName(), getRh());
				return getAlias() + getName() + " <= :" + getParameterName();
			}
			if (rh == null) {
				realparams.put(getParameterName(), getLh());
				return getAlias() + getName() + " >= :" + getParameterName();
			}
			realparams.put("from" + getParameterName(), getLh());
			realparams.put("to" + getParameterName(), getRh());
			return "(" + getAlias() + getName() + " between :from" + getParameterName()
					+ " and :to" + getParameterName() + ")";
		}

		public void clear() {
			lh = null;
			rh = null;
		}
	}

	public class NumberParameter extends Parameter {
		private Number lh;
		private Number rh;

		public NumberParameter(String name) {
			super(name);
		}

		public NumberParameter() {
			super();
		}

		public NumberParameter(String name, String association, String label) {
			super(name, association, label);
		}

		public Number getLh() {
			return lh;
		}

		public void setLh(Number lh) {
			this.lh = lh;
		}

		public Number getRh() {
			return rh;
		}

		public void setRh(Number rh) {
			this.rh = rh;
		}

		protected boolean hasValue() {
			return lh != null || rh != null;
		}

		public String getFilterString(Map<String, Object> realparams) {
			if (!hasValue()) {
				return "";
			}
			if (lh == null) {
				realparams.put(getParameterName(), getRh());
				return getAlias() + getName() + " <= :" + getParameterName();
			}
			if (rh == null) {
				realparams.put(getParameterName(), getLh());
				return getAlias() + getName() + " >= :" + getParameterName();
			}
			realparams.put("from" + getParameterName(), getLh());
			realparams.put("to" + getParameterName(), getRh());
			// TODO correct syntax
			return "(" + getAlias() + getName() + " between :from" + getParameterName()
					+ " and :to" + getParameterName() + ")";
		}

		public void clear() {
			lh = null;
			rh = null;
		}
	}

	@SuppressWarnings("unchecked")
	public class EnumParameter extends Parameter {
		private Object[] selected;
		private List<SelectItem> list = new ArrayList<SelectItem>();

		public EnumParameter(String name, Class clazz) {
			super(name);
			Object[] constants = clazz.getEnumConstants();
			for (Object literal : constants) {
				String label = getResourceBundle().getString(clazz.getSimpleName()
						+ "_" + literal);
				list.add(new SelectItem(literal, label));
			}
		}

		public EnumParameter() {
			super();
		}

		public EnumParameter(String name, String association, String label, Class clazz) {
			super(name, association, label);
			Object[] constants = clazz.getEnumConstants();
			for (Object literal : constants) {
				String label2 = getResourceBundle().getString(clazz.getSimpleName()
						+ "_" + literal);
				list.add(new SelectItem(literal, label2));
			}
		}

		public List<SelectItem> getList() {
			return list;
		}

		public void setSelected(Object[] selected) {
			this.selected = selected;
		}

		public Object[] getSelected() {
			return selected;
		}

		protected boolean hasValue() {
			return selected != null && selected.length != 0;
		}

		public String getFilterString(Map<String, Object> realparams) {
			if (!hasValue()) {
				return "";
			}

			boolean first = true;
			StringBuffer sb = new StringBuffer();
			sb.append(getAlias());
			sb.append(getName());
			sb.append(" in (");
			for (Object literal : selected) {
				if (first) {
					first = false;
				} else {
					sb.append(", ");
				}
				sb.append("'");
				sb.append(literal);
				sb.append("'");
			}
			sb.append(")");
			return sb.toString();
		}

		public void clear() {
			selected = null;
		}
	}
	
	/**
	 * Use for M2O and O2O associations
	 */
	public class EntityParameter extends Parameter {
		private String property;
		private String eql;
		transient private List<SelectItem> list = null;
		private Object[] selected;
		private boolean includeUnassigned = true;
		// some associations have too much data to display a list
		private String value;
		private boolean freeForm = false; // TODO simple filtered list ???

		protected final Long UNASSIGNED = new Long(-1);

		public EntityParameter(String name, String property, String eql) {
			super(name);
			this.property = property;
			this.eql = eql;
		}

		// TODO consider rerendering

		public EntityParameter(String name, String label, String property,
				String eql) {
			setName(name);
			setLabel(getResourceBundle().getString(label));
			this.property = property;
			this.eql = eql;
		}

		public List<SelectItem> getList() {
			if (list == null) {
				list = new ArrayList<SelectItem>();
				if (isIncludeUnassigned()) {
					list.add(new SelectItem(UNASSIGNED, "UNASSIGNED"));
				}
				
				Iterator results = getEntityManager().createQuery(eql).getResultList()
						.iterator();
				while (results.hasNext()) {
					Object[] row = (Object[]) results.next();
					list.add(new SelectItem(row[0], row[1].toString()));
				}
			}
			return list;
		}

		public void setSelected(Object[] selected) {
			this.selected = selected;
		}

		public Object[] getSelected() {
			return selected;
		}

		public String getProperty() {
			return property;
		}

		public void setProperty(String property) {
			this.property = property;
		}

		public String getValue() {
			return value;
		}

		public void setValue(String value) {
			this.value = value;
		}

		public boolean isFreeForm() {
			return freeForm;
		}

		public void setFreeForm(boolean freeForm) {
			this.freeForm = freeForm;
		}

		public boolean isIncludeUnassigned() {
			return includeUnassigned;
		}

		public void setIncludeUnassigned(boolean includeUnassigned) {
			this.includeUnassigned = includeUnassigned;
		}

		protected boolean hasValue() {
			return (selected != null && selected.length != 0) || !StringUtils.isEmpty(value);
		}

		protected boolean isUnassigned() {
			for (Object id : selected) {
				if (UNASSIGNED.toString().equals(id)) {
					return true;
				}
			}
			return false;
		}
		
		public String getFilterString(Map<String, Object> realparams) {
			if (!hasValue()) {
				return "";
			}

			if (isFreeForm()) {
				realparams.put(getParameterName(), "%" + getValue().toLowerCase() + "%");
				return "lower(" + getAlias() + getName() + "." + getProperty() + ") like :" + getParameterName();
			}
			
			if (isUnassigned()) {
				StringBuffer sb = new StringBuffer();
				sb.append(getAlias());
				sb.append(getName());
				sb.append(" is null");
				return sb.toString();
			}
			
			boolean first = true;
			StringBuffer sb = new StringBuffer();
			sb.append(getAlias());
			sb.append(getName());
			sb.append(".id in (");
			for (Object id : selected) {
				if (first) {
					first = false;
				} else {
					sb.append(", ");
				}
				sb.append(id);
			}
			sb.append(")");
			return sb.toString();
		}

		public void getJoin(StringBuilder join) {
			// String orderBy = "";
			// || orderBy.equals(getName()) && !filter on this
			// TODO move order by to filter object
			if (false) {
				// outer needed for ORs
				// outer not needed for ANDs
				String temp = " left outer join t." + getName();
				if (join.indexOf(temp) == -1) {
					join.append(temp);
				}
			}
		}

		public void clear() {
			value = null;
			list = null;
			selected = null;
		}
	}

	public class O2mEntityParameter extends EntityParameter {

		public O2mEntityParameter(String name, String property, String eql) {
			super(name, property, eql);
			setIncludeUnassigned(false);
		}

		public O2mEntityParameter(String name, String label, String property,
				String eql) {
			super(name, label, property, eql);
			setIncludeUnassigned(false);
		}

		public String getFilterString(Map<String, Object> realparams) {
			if (!hasValue()) {
				return "";
			}

			if (isFreeForm()) {
				realparams.put(getParameterName(), "%" + getValue().toLowerCase() + "%");
				return "lower(" + getAlias() + getName() + ") like :" + getParameterName();
			}

			if (isUnassigned()) {
				StringBuffer sb = new StringBuffer();
				sb.append(getName());
				sb.append(".");
				sb.append(getProperty());
				sb.append(" is null");
				return sb.toString();
			}

			boolean first = true;
			StringBuffer sb = new StringBuffer();
			sb.append(getName());
			sb.append(".");
			sb.append(getProperty());
			sb.append(".id in (");
			for (Object id : getSelected()) {
				if (first) {
					first = false;
				} else {
					sb.append(", ");
				}
				sb.append(id);
			}
			sb.append(")");
			return sb.toString();
		}

		public void getJoin(StringBuilder join) {
			if (getSelected() == null || getSelected().length == 0) {
				return;
			} else {
				// TODO outer or inner ???
				String temp = " join t." + getName() + " " + getName();
				if (join.indexOf(temp) == -1) {
					join.append(temp);
				}
			}
		}
	}

	public class M2mEntityParameter extends O2mEntityParameter {

		public M2mEntityParameter(String name, String property, String eql) {
			super(name, property, eql);
			setIncludeUnassigned(false);
		}

		public M2mEntityParameter(String name, String label, String property,
				String eql) {
			super(name, label, property, eql);
			setIncludeUnassigned(false);
		}

		public String getFilterString(Map<String, Object> realparams) {
			if (!hasValue()) {
				return "";
			}

			if (isFreeForm()) {
				realparams.put(getParameterName(), "%" + getValue().toLowerCase() + "%");
				return "lower(" + getAlias() + getName() + ") like :" + getParameterName();
			}

			boolean first = true;
			StringBuffer sb = new StringBuffer();
			sb.append(getName());
			sb.append(".id in (");
			for (Object id : getSelected()) {
				if (first) {
					first = false;
				} else {
					sb.append(", ");
				}
				sb.append(id);
			}
			sb.append(")");
			return sb.toString();
		}
	}

	// -------------------------------------------------------
	// resources needed
	// -------------------------------------------------------

	transient private Class entityClass;
	transient private ResourceBundle resourceBundle;
	transient private EntityManager entityManager;

	public void setEntityClass(Class entityClass) {
		this.entityClass = entityClass;
	}

	public void setResourceBundle(ResourceBundle resourceBundle) {
		this.resourceBundle = resourceBundle;
	}

	public ResourceBundle getResourceBundle() {
		if (resourceBundle == null) {
			resourceBundle = org.jboss.seam.core.ResourceBundle.instance();
		}
		return resourceBundle;
	}
	
	public EntityManager getEntityManager() {
		if (entityManager == null || !entityManager.isOpen()) {
			entityManager = EntityPersistenceContextMap.instance()
					.getEntityManagerNameFor(entityClass);
		}
		return entityManager;
	}

	// -------------------------------------------------------
	// paramater metadata
	// -------------------------------------------------------

	private List<StringParameter> strings = new ArrayList<StringParameter>();

	private List<BooleanParameter> booleans = new ArrayList<BooleanParameter>();

	private List<DateParameter> dates = new ArrayList<DateParameter>();

	private List<NumberParameter> numbers = new ArrayList<NumberParameter>();

	private List<EnumParameter> enums = new ArrayList<EnumParameter>();

	private List<EntityParameter> m2os = new ArrayList<EntityParameter>();

	private List<EntityParameter> m2ms = new ArrayList<EntityParameter>();

	private List<EntityParameter> o2ms = new ArrayList<EntityParameter>();

	public List<StringParameter> getStrings() {
		return strings;
	}

	public List<BooleanParameter> getBooleans() {
		return booleans;
	}

	public List<DateParameter> getDates() {
		return dates;
	}

	public List<NumberParameter> getNumbers() {
		return numbers;
	}

	public List<EnumParameter> getEnums() {
		return enums;
	}

	public List<EntityParameter> getM2os() {
		return m2os;
	}

	public List<EntityParameter> getM2ms() {
		return m2ms;
	}

	public List<EntityParameter> getO2ms() {
		return o2ms;
	}
}
